package nz.ac.massey.groupproject.ecs2;

import java.util.ArrayList;


// FIXME spaghetti fes
//
// btw, FIXME this most definitely doesn't work.  However, it is sufficiently good at pretending to work.
// Failure cases includes:
//    when entity spawns inside a wall
//    when entity collides with multiple walls at the same time
//    when entity clips into a wall but doesn't have enough speed to clip out after direction is reflected
//    ...
public class LinearBouncebackMotionSystem implements ECSSystem {

	public void tick(ECSWorld world, GameState gdata, ECSEntity ent, Position pos, LinearBouncebackMotion vel) {
		final int  MIN_X = gdata.MINX,  MAX_X = gdata.MAXX;
		final int  MIN_Y = gdata.MINY,  MAX_Y = gdata.MAXY;

		if (vel.vx == 0 && vel.vy == 0) return;
		assert vel.vx <= 50 && vel.vy <= 50 && vel.speed2 <= 2500 : "entity is moving too fast";

		// movement
		pos.x += vel.vx;
		pos.y += vel.vy;

		// bounce back  (Order of x and y doesn't matter)
		if (pos.x < MIN_X) { int xExcess = MIN_X - pos.x;   pos.x = MIN_X + xExcess;   vel.vx *= -1; }
		if (pos.x > MAX_X) { int xExcess = pos.x - MAX_X;   pos.x = MAX_X - xExcess;   vel.vx *= -1; }
		if (pos.y < MIN_Y) { int yExcess = MIN_Y - pos.y;   pos.y = MIN_Y + yExcess;   vel.vy *= -1; }
		if (pos.y > MAX_Y) { int yExcess = pos.y - MAX_Y;   pos.y = MAX_Y - yExcess;   vel.vy *= -1; }

		// obstacles    (TODO eventually this will replace the 4 world border checks)
		var relevantWalls   = new ArrayList<ECSEntity>();
		var redzones        = new ArrayList<Polygon>();
		var rzVertexMapping = new ArrayList<int[]>();
		var oldpos          = new Position(pos.x - vel.vx, pos.y - vel.vy);  // may be a fake position due to bouncing once already
		boolean collide = checkCollision(relevantWalls, redzones, rzVertexMapping, gdata.walls, ent, oldpos, pos);

		//if (collide) {
		//	if (ent.has(Appearance.class))
		//		ent.set(Appearance.DEBUG_TINY_DOT1);
		//}

		final var velvec = new Vec2l(vel.vx, vel.vy);
		final int movedir8 = velvec.getOctadir();
		assert movedir8 != -1 : "We don't have time to implement special angle velocity"; // TODO

		// TODO handle edge case caused by rounding wall direction
		for (int i = 0; i < relevantWalls.size(); i++) {
			var wall = relevantWalls.get(i);
			var wpoly = ((PolygonComponent) wall.get(PolygonComponent.class)).poly;
			var wpos  =  (Position)         wall.get(Position        .class);

			var redzone = redzones.get(i);
			int[] vmap = rzVertexMapping.get(i);

			int[] rzIndex = redzone.findCollisionSite(velvec);
			assert rzIndex != null;

			int wallInd1 = -1, wallInd2 = -1;
			// sum point -> original point
			// sum edge -> original edge / point
			if (rzIndex.length == 1) {
				wallInd1 = vmap[rzIndex[0]];
			} else {
				wallInd1 = vmap[rzIndex[0]];
				wallInd2 = vmap[rzIndex[1]];
				if (wallInd1 == wallInd2) wallInd2 = -1;
			}

			final int newdir8;

			// colliding on wall corner
			if (-1 == wallInd2) {
				// give up.           TODO implement bounce back
				final var numVtx = wpoly.vs.size();
				final var P = wpoly.getV( (wallInd1 - 1 + numVtx) % numVtx );
				final var Q = wpoly.getV( wallInd1 );
				final var R = wpoly.getV( (wallInd1 + 1) % numVtx );

				final var wallPQ = Q.sub(P);
				final var wallQR = R.sub(Q);
				assert wallPQ.getOctadir() != -1 : "We don't have time to implement special angle walls";
				assert wallQR.getOctadir() != -1 : "We don't have time to implement special angle walls";
				final int wallPQ8 = wallPQ.getOctadir();
				final int wallQR8 = wallQR.getOctadir();

				int normal8 = ((wallPQ8 + wallQR8) / 2) % 8;
				Vec2l normal = new Vec2l(Vec2l.OCTADIR_X[normal8], Vec2l.OCTADIR_Y[normal8]);
				if (normal.dot(wallPQ) < 0) {
					normal8 = (normal8 + 4) % 8;
					normal = normal.neg();
				}
				final int wall8 = (normal.cprod(velvec) >= 0) ? wallPQ8 : wallQR8;

				newdir8 = (wall8 * 2 - movedir8 + 8) % 8;

			// colliding on wall side
			} else {
				final var P = wpoly.getV(wallInd1);
				final var Q = wpoly.getV(wallInd2);
				final int wall16 = Q.sub(P).roundToHexadir();
				assert Q.sub(P).getOctadir() != -1 : "We don't have time to implement special angle walls"; // TODO

				newdir8 = (wall16 - movedir8 + 8) % 8;  // out = 2*wall - in
			}

			final long dx = Vec2l.OCTADIR_X[newdir8];
			final long dy = Vec2l.OCTADIR_Y[newdir8];

			final long proj = (newdir8 % 2 == 0) ? sqrtRound(vel.speed2)
							     : sqrtRound((vel.speed2 + 1) / 2);  // slightly inaccurate, but we are already losing so much accuracy, losing a little bit more doesn't hurt.
			vel.vx = (int)(dx * proj);
			vel.vy = (int)(dy * proj);

			break; // TODO handle multiple walls
		}

	}

	// TODO pick a better name
	// FIXME code duplication
	//
	// Return -1 if overlapping with a wall
	// Else return 0 if scratching a wall
	// Return 1 if not touching any walls
	private boolean checkCollision(ArrayList<ECSEntity> relevantWalls, ArrayList<Polygon> relevantRedzones, ArrayList<int[]> rzVertexMapping, // these two are output parameters
	                               ArrayList<ECSEntity> allWalls, ECSEntity ent, Position oldpos, Position newpos) {

		final var epoly = (PolygonComponent)  ent.get(PolygonComponent.class);

		boolean collide = false;
		for (var wall : allWalls) {
			final var wpoly = (PolygonComponent) wall.get(PolygonComponent.class);
			final var wpos  = (Position)         wall.get(Position        .class);
			assert wpoly != null && wpos != null;

			Polygon redzone;
			int[]   vmap;
			if (null == epoly) {
				redzone = wpoly.poly;
				vmap = new int[redzone.vs.size()];
				for (int i = 0; i < vmap.length; i++)  vmap[i] = i;
			} else {
				vmap = new int[ wpoly.poly.vs.size() + epoly.poly.vs.size() ];
				redzone = Polygon.minkowskiDiff(wpoly.poly, epoly.poly, vmap, null);
			}
			final var P = new Vec2l(newpos.x - wpos.x, newpos.y - wpos.y);
			if (redzone.whereIs(P) >= 0) continue;

			collide = true;

			relevantWalls.add(wall);
			relevantRedzones.add(redzone.translate(wpos.x - oldpos.x, wpos.y - oldpos.y));
			rzVertexMapping.add(vmap);
		}
		return collide;
	}

	// break ties by returning ceil(sqrt(n))
	// precondition: n >= 0     (and n*4 plus a little doesn't overflow)
	// TODO precompute
	// TODO move to common utils
	public static final long sqrtRound(long n) {
		assert n >= 0 : "Taking round(sqrt(n)) of a negative number";
		if (n < 2) return n;

		long high = n, low = 0;         // invariant: low < sqrt < high (strict inequality)
		while (high - low > 1) {
			long mid = (high + low) / 2;
			if      (mid * mid > n)   high = mid;
			else if (mid * mid < n)   low  = mid;
			else return mid;
		}
		return ((high+low) * (high+low)  >  n * 4)   ?  low  : high;
	}
}
